/*****************************************************************
JADE - Java Agent DEvelopment Framework is a framework to develop 
multi-agent systems in compliance with the FIPA specifications.
Copyright (C) 2000 CSELT S.p.A. 

GNU Lesser General Public License

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation, 
version 2.1 of the License. 

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the
Free Software Foundation, Inc., 59 Temple Place - Suite 330,
Boston, MA  02111-1307, USA.
 *****************************************************************/

package agents;

import gui.HomeGUI;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Random;

import statics.TradingConst;

import jade.core.Agent;
import jade.core.AID;
import jade.core.behaviours.*;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.domain.FIPAAgentManagement.Unauthorised;

public class HomeAgent extends TickListenerAgent {
	
	transient protected HomeGUI myUI;
	
	
	// The title of the book to buy
	private Double PowerRequired;
	// The list of known seller agents
	private AID[] retailerAgents;
	private AID		currentRetailer;
	private double  dLoyatyAdjusmentMultiplier = 1.0;
	private final double dLoyatyAdjustmentIncrement = 0.015;
	private ArrayList<Appliance> appliances;
	
	private boolean bIsDealing = false;
	 
	//private final long EnergyUpdatePeriod_Minute = 30;
	
	protected double MonthlyEnergySpendingThresholdDollars = 100.0;

	private double IdealAverageSpendingPerPeriod;
	
	protected int TicksToUpdateLoad = Math.max((int) (TradingConst.HOME_AGENT_MIN_TO_UPDATE_LOAD*TradingConst.SEC_IN_MIN*TradingConst.SEC_TO_MS
					/(TradingConst.Tick_TO_MS*TradingConst.ACCELERATION_RATE)),1);
	
	protected int TicksToUpdateAgents = Math.max((int) (TradingConst.HOME_AGENT_MIN_TO_UPDATE_RETAILERS*TradingConst.SEC_IN_MIN*TradingConst.SEC_TO_MS
			/(TradingConst.Tick_TO_MS*TradingConst.ACCELERATION_RATE)),1);


	private int NextExpectedUpdateLoadTicks = TicksToUpdateLoad;
	
	// Put agent initializations here
	protected void setup() {
		super.setup();
		// Printout a welcome message
		System.out.println("Hallo! Home-agent "+getAID().getName()+" is ready.");
		
		//Caculate spending per period
		setIdealAverageSpendingPerPeriod((MonthlyEnergySpendingThresholdDollars));
		//currentRetailer = new AID();
		currentRetailer = null;
		appliances = new ArrayList<HomeAgent.Appliance>();
		Boolean args[] = (Boolean[])getArguments();
		if(args.length > 0
				&& args[0] == true)
		{
			myUI = new HomeGUI(this);
			myUI.setVisible(true);
		}else
		{
			AddAppliance("Fridge", 500.0,true);
			AddAppliance("Lights", 300.0,true);
			AddAppliance("Solar Panel", -200.0,true);
		}
		
	}
	

	@Override
	protected void OnTickAction() {
		// TODO Auto-generated method stub
		if(CurrentTickCount%TicksToUpdateAgents == 0)
		{
			addBehaviour(new UpdateRetailersBehaviour());
		}
		if((CurrentTickCount == NextExpectedUpdateLoadTicks))
		{
			Random rand = new Random();
			NextExpectedUpdateLoadTicks += ((int)(TicksToUpdateLoad*0.8 + rand.nextInt((int)(TicksToUpdateLoad* 0.4)) ));
			addBehaviour(new LoadUpdateBehaviour());
		}
	}
	
	public boolean AddAppliance(String name, Double power, Boolean isON)
	{
		if(!appliances.contains(new Appliance(name, power,isON)))
		{
			appliances.add(new Appliance(name, power,isON));
			//@todo: implement real time update
			NextExpectedUpdateLoadTicks =  CurrentTickCount + 5; //Math.max(arg0, 5)
			//TODO currently only periodically check power required
			//addBehaviour(new LoadUpdateBehaviour());
			return true;
		}
		return false;
	}
	public void ChangeApplianceState(String name, Boolean isOn)
	{
		int apIndex = appliances.indexOf(new Appliance(name));
		appliances.get(apIndex).IsOn = isOn;
		//@todo: implement real time update
		NextExpectedUpdateLoadTicks =  CurrentTickCount + 5; //Math.max(arg0, 5)
	}
	// Put agent clean-up operations here
	protected void takeDown() {
		// Printout a dismissal message
		//System.out.println("Home-agent "+getAID().getName()+" terminating.");
	}
	protected void PostPurchaseUpdate(double rateInDollarPerKWh, String purchasedRetailer) {
		// TODO Auto-generated method stub
		double amountSpentThisPeriod = rateInDollarPerKWh*PowerRequired*(TradingConst.HOME_AGENT_MIN_TO_UPDATE_LOAD/TradingConst.MIN_IN_HOUR)*TradingConst.WATT_TO_KW;
		boolean isExceeding = (getIdealAverageSpendingPerPeriod() > amountSpentThisPeriod);
		if(myUI != null)
		{
			myUI.UpdateFromHomeAgent(CurrentTickCount, 
					PowerRequired, 
					rateInDollarPerKWh, 
					amountSpentThisPeriod, 
					getIdealAverageSpendingPerPeriod(), 
					isExceeding,
					purchasedRetailer);
		}
		
	}
	private class UpdateRetailersBehaviour extends OneShotBehaviour {

		@Override
		public void action() {
			//System.out.println("Trying to buy "+PowerRequired);
			// Update the list of seller agents
			DFAgentDescription template = new DFAgentDescription();
			ServiceDescription sd = new ServiceDescription();
			sd.setType("power-retailer");
			template.addServices(sd);
			try {
				DFAgentDescription[] result = DFService.search(myAgent, template); 
				//System.out.println("Found the following retailer agents:");
				retailerAgents = new AID[result.length];
				for (int i = 0; i < result.length; ++i) {
					retailerAgents[i] = result[i].getName();
					//System.out.println(retailerAgents[i].getName());
				}
				Random rand = new Random();
				if(currentRetailer == null)
				{
					currentRetailer = result[rand.nextInt(result.length)].getName();
				}
			}
			catch (FIPAException fe) {
				fe.printStackTrace();
			}
		}
	}

	private class LoadUpdateBehaviour extends OneShotBehaviour {

		@Override
		public void action() {
			PowerRequired = 0.0;
			
			for(Appliance ap:appliances)
			{
				if(ap.IsOn)
				{
					PowerRequired += ap.PowerDemand;
				}
			}
			//System.out.println("The amount of power required is "+PowerRequired);
			
			//trigger appropriate buy/sell
			if(PowerRequired != 0 && retailerAgents != null )
			{
				if(!bIsDealing)
				{
					if(retailerAgents.length >0 )
					{
						myAgent.addBehaviour(new TradingRequestPerformer());
					}
				}else
				{
					//TODO what to do with current sale/purchase
					//would they be cancelled? Added to current need?
					//Simply wait for next period
					
					//System.out.println("Another purchase is in place ");
				}
					
			}
			else if(PowerRequired == 0)
			{
				PostPurchaseUpdate(0,"Power required is ZERO");
			}
			
			//else nothing
			
		}
		
	}

	/**
	   Inner class RequestPerformer.
	   This is the behaviour used by Home agent to buy and sell power   
	 */
	private class TradingRequestPerformer extends Behaviour {
		private AID bestRetailer; // The agent who provides the best offer 
		private Double bestPrice;  // The best offered price
		private int repliesCnt = 0; // The counter of replies from seller agents
		private MessageTemplate mt; // The template to receive replies
		private int step = 0;
		private boolean bIsPurchasingTransaction;
		
		//These will help working out the price for the energy consumption
		//Amount = Period*Power*Price
		private int StartTradingTick;
		private int EndTradingTick;
		private String purchasedRetailer = "";
		
		public void action() {
			bIsDealing = true;
			
			switch (step) {
			case 0:
				
				bIsPurchasingTransaction = (PowerRequired > 0);
				
				//prepare starting price
				if(bIsPurchasingTransaction)
				{
					//buying, starts with highest
					//wanna get lowest
					bestPrice =  Double.MAX_VALUE;
				}else
				{
					//selling, starts with lowest
					//want to get highest
					bestPrice = Double.MIN_VALUE;
				}
				
				// Send the cfp to all sellers
				ACLMessage cfp = new ACLMessage(ACLMessage.CFP);
				for (int i = 0; i < retailerAgents.length; ++i) {
					cfp.addReceiver(retailerAgents[i]);
				} 
				cfp.setContent(PowerRequired.toString());
				cfp.setConversationId("power-trade");
				cfp.setReplyWith("cfp"+System.currentTimeMillis()); // Unique value
				myAgent.send(cfp);
				// Prepare the template to get proposals
				mt = MessageTemplate.and(MessageTemplate.MatchConversationId("power-trade"),
						MessageTemplate.MatchInReplyTo(cfp.getReplyWith()));
				
				//System.out.println("Request for " + PowerRequired.toString() + " sent");
				step = 1;
				break;
			case 1:
				// Receive all proposals/refusals from seller agents
				ACLMessage reply = myAgent.receive(mt);
				if (reply != null) {
					// Reply received
					if (reply.getPerformative() == ACLMessage.PROPOSE) {
						// This is an offer 
						Double dUnadjustedPrice = Double.parseDouble(reply.getContent());
						dUnadjustedPrice = ( (int)(dUnadjustedPrice.doubleValue()*1000))/1000.00;
						Double price = dUnadjustedPrice.doubleValue();
						
						System.out.println(myAgent.getLocalName() + " Offer for " + PowerRequired.toString() + " of " + price.toString() + " received");
						
						if(currentRetailer!= null)
						{
							if(currentRetailer.getLocalName().equalsIgnoreCase(reply.getSender().getLocalName()))
							{
								if(dLoyatyAdjusmentMultiplier < 1.0)
								{
									dLoyatyAdjusmentMultiplier = 1.0;
								}
								if(PowerRequired >0)
								{
									price *= 0.95*dLoyatyAdjusmentMultiplier;
								}else
								{
									price *= 1.05/dLoyatyAdjusmentMultiplier;
								}
								price = Math.min(price, dUnadjustedPrice);
								System.out.println(myAgent.getLocalName() + "Price adjusted " + price);
							}
						}
						if (bestRetailer == null || 
								((price < bestPrice && bIsPurchasingTransaction)
								||(price > bestPrice && !bIsPurchasingTransaction)
										)) {
							// This is the best offer at present
							bestPrice = price;
							bestRetailer = reply.getSender();
							
						}
					}
					repliesCnt++;
					if (repliesCnt >= retailerAgents.length) {
						// We received all replies
						step = 2; 
						if(bestRetailer.getLocalName().equalsIgnoreCase(currentRetailer.getLocalName()))
						{
							//dLoyatyAdjusmentMultiplier += dLoyatyAdjustmentIncrement;
							//System.out.println("Adj for = " + myAgent.getLocalName()+ " " +dLoyatyAdjusmentMultiplier
							//		+ " price " + bestPrice  );
						}
					}
				}
				else {
					block();
				}
				break;
			case 2:
				// Send the purchase order to the seller that provided the best offer
				ACLMessage order = new ACLMessage(ACLMessage.ACCEPT_PROPOSAL);
				order.addReceiver(bestRetailer);
				order.setContent(PowerRequired.toString());
				order.setConversationId("power-trade");
				order.setReplyWith("order"+System.currentTimeMillis());
				myAgent.send(order);
				// Prepare the template to get the purchase order reply
				mt = MessageTemplate.and(MessageTemplate.MatchConversationId("power-trade"),
						MessageTemplate.MatchInReplyTo(order.getReplyWith()));
				step = 3;
				break;
			case 3:      
				// Receive the purchase order reply
				reply = myAgent.receive(mt);
				if (reply != null) {
					// Purchase order reply received
					if (reply.getPerformative() == ACLMessage.INFORM) {
						// Purchase successful. We can terminate
						//System.out.println(PowerRequired+" successfully purchased from agent "+reply.getSender().getName());
						//System.out.println("Price = "+bestPrice);
						//((HomeAgent)myAgent).ExceedingIdealUsage(bestPrice, PowerRequired, TradingConst.HOME_AGENT_MIN_TO_UPDATE_LOAD
						//		, IdealAverageSpendingPerPeriod);
						purchasedRetailer = reply.getSender().getLocalName();
						
						if(!purchasedRetailer.equalsIgnoreCase(currentRetailer.getLocalName()))
						{
							//dLoyatyAdjusmentMultiplier -= dLoyatyAdjustmentIncrement;
							dLoyatyAdjusmentMultiplier = 1.0;
							System.out.println("Adj reseted = " + myAgent.getLocalName());//+ " " +dLoyatyAdjusmentMultiplier);
							
						}
						currentRetailer = reply.getSender();
					}
					else {
						//System.out.println("Attempt failed: insufficient supply");
					}

					// TODO: need to attempt to try again if purchase failed
					step = 4;
				}
				else {
					block();
				}
				break;
			}        
		}

		public boolean done() {
			if (step == 2 && bestRetailer == null) {
				//System.out.println("Attempt failed: "+PowerRequired+" not available for sale");
				// TODO failed to purchase, update accordingly
				PostPurchaseUpdate(0.0,"Power not available");
			}
			bIsDealing = !(((step == 2 && bestRetailer == null) || step == 4));
			if(!bIsDealing)
			{
				//done - notify
				PostPurchaseUpdate(bestPrice,purchasedRetailer);
			}
			return !bIsDealing;
		}
	}  // End of inner class RequestPerformer
	
	private class Appliance implements Comparable<Appliance>
	{
		public String ApplianceName = "";
		public Double PowerDemand = 0.0; //+ means require power, - means output power
		public Boolean IsOn = false;
		public Appliance(String name, Double power, Boolean isOn)
		{
			ApplianceName = name;
			PowerDemand = power;
			IsOn = isOn;
		}
		public Appliance (String name)
		{
			ApplianceName = name;
			PowerDemand = 0.0;
			IsOn = false;
		}
		@Override
		public int compareTo(Appliance arg0) {
			// TODO Auto-generated method stub
			if(arg0 == null)
			{
				return -1;
			}
			return arg0.ApplianceName.compareTo(ApplianceName);
		}
		@Override
		public boolean equals(Object o)
		{
			Appliance ap = (Appliance)o;
			if(ap == null)
			{
				return false;
			}
			return (ap.ApplianceName.equalsIgnoreCase(ApplianceName));
		}
	}

	public void SetMonthlyBudget(double budgetInDollars) {
		MonthlyEnergySpendingThresholdDollars = budgetInDollars;
		setIdealAverageSpendingPerPeriod(budgetInDollars);
	}


	public double getIdealAverageSpendingPerPeriod() {
		return IdealAverageSpendingPerPeriod;
	}


	public void setIdealAverageSpendingPerPeriod(
			double monthlyIdealSpending) {
		IdealAverageSpendingPerPeriod = monthlyIdealSpending/
										TradingConst.DAYS_IN_MONTH/
										TradingConst.HOURS_IN_DAY/
										(TradingConst.MIN_IN_HOUR/
										TradingConst.HOME_AGENT_MIN_TO_UPDATE_LOAD);
	}

	
}
